Skip to content

fix(vm): complete Node 26 parity - #7382

Open
TheHypnoo wants to merge 7 commits into
mainfrom
fix/node-vm-node26-parity
Open

fix(vm): complete Node 26 parity#7382
TheHypnoo wants to merge 7 commits into
mainfrom
fix/node-vm-node26-parity

Conversation

@TheHypnoo

@TheHypnoo TheHypnoo commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Complete Perry's Node.js 26.5.0 compatibility for node:vm across contexts, scripts, compiled functions, cross-realm values, metadata, and experimental modules.

Changes

  • Implement context-backed Script, runIn*Context, and compileFunction evaluation with persistent lexical state, strict writes, exact call arguments, and context extensions.
  • Complete context proxying, descriptors, validation, cached-data metadata, error locations, and cross-realm prototype identity.
  • Complete experimental Module, SourceTextModule, and SyntheticModule lifecycle, namespace, evaluation, and error behavior.
  • Remove the obsolete prototype-equality fold and make explicit realm prototypes authoritative for instanceof.
  • Keep VM heap state thread-local and rooted across moving-GC allocation points.

Related issue

Fixes #6768

Test plan

PATH=/Users/sergigonzalez/.nvm/versions/node/v26.5.0/bin:$PATH ./run_parity_tests.sh --suite node-suite --module vm
cargo check -p perry-hir -p perry-codegen -p perry-runtime --features dyn-eval
cargo test -p perry-hir
cargo test -p perry-runtime --features dyn-eval dyn_eval::tests
cargo test -p perry-runtime --features dyn-eval node_vm::tests::context_values_keep_vm_realm_prototypes
cargo fmt --all -- --check
python3 scripts/addr_class_inventory.py
python3 scripts/check_test_registration.py
./scripts/check_file_size.sh

Final node:vm report (parity_report_20260804_152948.json):

Parity Pass:   64
Parity Fail:   0
Compile Fail:  0
Crashed:       0
Skipped:       0
Parity Rate:   100.0%

./scripts/pre-tag-check.sh --quick passes the affected formatting, file-size, GC store-site, and address-classification gates. Its public benchmark freshness check remains red because the committed public artifact already differs from the current benchmark inputs; this PR does not modify benchmark inputs, artifacts, or runners.

Checklist

  • No tests, fixtures, expected outputs, runners, skips, or timeouts changed
  • No workspace version, CLAUDE.md, or CHANGELOG.md changes
  • Conventional commits only
  • Full node:vm gate passes with zero non-PASS counters
  • Read CONTRIBUTING.md and agreed to the Code of Conduct

Summary by CodeRabbit

  • New Features
    • Expanded node:vm compatibility with Node.js 26.5.0, including context options, scripts, compiled functions, cached-data metadata, and experimental VM modules.
    • Added script execution, direct eval, persistent bindings, object-backed environments, and context-specific globals and prototypes.
    • Added VM module linking, evaluation, namespaces, synthetic modules, and top-level-await support.
    • Improved WebAssembly and code-generation restriction handling.
  • Bug Fixes
    • Corrected strict-mode assignments, prototype comparisons, instanceof behavior, function source display, and VM error location reporting.
    • Improved isolation between VM contexts and handling of vm.constants.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Node VM parity now covers context options, dynamic scripts, direct eval, compileFunction, realm-specific prototypes, source metadata, WebAssembly restrictions, and experimental VM modules. Native bindings, descriptors, and runtime error reporting were updated.

Changes

Node VM parity

Layer / File(s) Summary
Dynamic evaluation and object environments
crates/perry-runtime/src/dyn_eval/*
Dynamic evaluation now uses context-specific globals and intrinsics, delegated object environments, strict assignments, direct eval, code-generation restrictions, and realm-specific prototypes.
Interpreter construction and execution
crates/perry-runtime/src/dyn_eval/interp.rs, crates/perry-runtime/src/dyn_eval/mod.rs
Interpreted functions now preserve strictness, realm state, invocation arguments, persistent script bindings, and direct-eval environments.
VM context and script execution
crates/perry-runtime/src/node_vm.rs, crates/perry-runtime/src/error.rs
VM execution now validates options and source, tracks context and compiled-function state, enforces code-generation settings, reports source locations, and maintains GC-safe state.
Experimental VM modules
crates/perry-runtime/src/node_vm/modules.rs
Source and synthetic modules now support construction, linking, instantiation, evaluation, namespaces, cached-data queries, status checks, and asynchronous dependency checks.
Native VM API and object contracts
crates/perry-codegen/..., crates/perry-runtime/src/node_submodules/..., crates/perry-runtime/src/object/...
Native VM signatures now pass context options and brand scripts. VM descriptors, constants, namespaces, prototypes, and WebAssembly checks use updated contracts.
Realm metadata and validation
crates/perry-hir/src/lower/lower_expr/arm_bin.rs, crates/perry-runtime/src/object/instanceof.rs, crates/perry-runtime/src/dyn_eval/tests.rs, changelog.d/7382-node-vm-node26-parity.md
Equality lowering, prototype-chain checks, closure source lookup, and runtime source locations use updated metadata. Tests cover fresh realms and cross-realm behavior. The changelog records Node.js 26.5.0 parity.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • PerryTS/perry#6473 — Related node:vm context, script, compileFunction, cached-data, and module behavior.

Suggested labels: parity, rust

Sequence Diagram(s)

sequenceDiagram
  participant VM_API
  participant NodeVM
  participant DynEval
  participant RealmIntrinsics
  participant VM_Module
  VM_API->>NodeVM: create context or compile source
  NodeVM->>DynEval: execute script or direct eval
  DynEval->>RealmIntrinsics: resolve globals and attach prototypes
  NodeVM->>VM_Module: link or evaluate VM module
  DynEval-->>NodeVM: return value or evaluation error
  VM_Module-->>VM_API: return namespace or evaluation promise
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: completing Node.js 26 parity for the vm module.
Description check ✅ Passed The description includes the required summary, changes, issue, test plan, and checklist sections with specific implementation and validation details.
Linked Issues check ✅ Passed The changes address the linked #6768 parity objectives across contexts, scripts, validation, cross-realm behavior, and experimental modules.
Out of Scope Changes check ✅ Passed The changed files support the linked node:vm parity objectives, including evaluation, realms, modules, metadata, and runtime integration.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/node-vm-node26-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@TheHypnoo TheHypnoo added the type:bug Bug fix label Aug 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
crates/perry-runtime/src/dyn_eval/env.rs (1)

279-317: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

GC-managed values are cached in plain Rust locals and reused after allocating calls. All three sites copy a NaN-boxed f64 into an ordinary local, then call a function that can allocate and relocate the referenced object, then reuse the stale copy. Rust locals are neither GC roots nor pins in this runtime, so the copy is not rewritten when the object moves. The shared remediation is to reload the value from its rooted slot or RuntimeHandleScope handle at every use after a possible collection point.

  • crates/perry-runtime/src/dyn_eval/env.rs#L279-L317: drop the current local and read root_get(cur_idx) at lines 280, 299, and 311, matching the other uses in the same loop.
  • crates/perry-runtime/src/dyn_eval/env.rs#L398-L411: re-derive the bindings object with env_object_bindings(root_get(cur_idx)) before each object_write_binding call, because object_has_binding allocates an interned key string.
  • crates/perry-runtime/src/object/class_registry/construct.rs#L153-L162: drop the constructor_value local and pass constructor.get_nanbox_f64() to synthetic_class_id_for_function and install_script_prototypes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/env.rs` around lines 279 - 317, Reload
GC-managed values from rooted storage after any allocating call instead of
reusing stale Rust locals: in crates/perry-runtime/src/dyn_eval/env.rs:279-317,
remove current and use root_get(cur_idx) at the indicated accesses; in
crates/perry-runtime/src/dyn_eval/env.rs:398-411, re-derive
env_object_bindings(root_get(cur_idx)) before each object_write_binding call,
while object_has_binding may allocate; in
crates/perry-runtime/src/object/class_registry/construct.rs:153-162, remove
constructor_value and pass constructor.get_nanbox_f64() directly to
synthetic_class_id_for_function and install_script_prototypes.

Sources: Coding guidelines, Learnings

crates/perry-runtime/src/object/descriptors.rs (1)

1627-1632: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make vm namespace properties configurable except when the module is specifically vm.constants.

The current condition sets configurable: false for every vm key, including runInNewContext, so Object.defineProperty(vm, 'runInNewContext', ...) and delete vm.runInNewContext fail unlike Node. This does not affect fs.constants keys, which are explicitly handled above and not covered by the tail case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/descriptors.rs` around lines 1627 - 1632,
Update the configurable argument in the build_data_descriptor call for the vm
namespace tail case so properties are configurable for vm and non-vm modules,
but remain non-configurable when module_name is exactly "vm.constants". Preserve
the separate fs.constants handling above.
crates/perry-runtime/src/node_vm.rs (1)

1496-1524: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unrooted heap values cross allocating writes in both VM metadata-cache registration paths. Both paths allocate an object or closure, register it in a process-global metadata map, and then perform further allocating field writes while holding only a bare raw pointer or NaN-boxed f64 local. Rust stack locals are not scanned and raw pointer locals are neither roots nor pins, so a moving collection during those writes makes the local stale. scan_vm_roots_mut repairs the map keys through visit_metadata_usize_slot, but it cannot repair the locals or the returned value.

  • crates/perry-runtime/src/node_vm.rs#L1496-L1524: root the js_object_alloc result through RuntimeHandleScope, derive the map key and every set_field receiver from the reloaded handle, and build the returned value after the final write.
  • crates/perry-runtime/src/node_vm.rs#L1739-L1757: root the with_source_location result, reload it before set_builtin_closure_length, before the compiled_function_sources insert, and before each set_value_field call.

As per coding guidelines, GC-managed values must remain rooted across every possible collection point, and root stores must dominate subsequent allocating sites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm.rs` around lines 1496 - 1524, Root the
object returned by js_object_alloc in the node_vm.rs metadata-cache path at
lines 1496-1524 with RuntimeHandleScope, reload the handle for the metadata-map
key and every set_field receiver, and construct the returned value only after
the final allocating write. In the sibling node_vm.rs path at lines 1739-1757,
root the with_source_location result and reload it before
set_builtin_closure_length, the compiled_function_sources insert, and each
set_value_field call; preserve these roots across every possible collection
point.

Source: Coding guidelines

🧹 Nitpick comments (7)
crates/perry-runtime/src/dyn_eval/tests.rs (2)

993-997: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the named Promise class-id constant.

Line 995 hardcodes 0xFFFF_0027. crates/perry-runtime/src/object/instanceof.rs defines CLASS_ID_PROMISE for this value. If the reserved id changes, the literal silently tests the wrong class. Reference the constant, as the neighbouring assertion already does with crate::error::CLASS_ID_TYPE_ERROR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/tests.rs` around lines 993 - 997, The
js_instanceof call in the assertion hardcodes the Promise class id as
0xFFFF_0027, but the constant CLASS_ID_PROMISE is already defined in
crate::object and should be referenced instead. Replace the hardcoded literal
0xFFFF_0027 with crate::object::CLASS_ID_PROMISE to ensure the test
automatically stays in sync if the class id constant is updated.

1000-1012: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for strict-mode object-environment writes.

This PR threads a new strict flag through env::assign, object_write_binding, and bridge::set_index. The added tests exercise only sloppy writes.

Add a test that evaluates a "use strict" script whose assignment targets a non-writable or frozen sandbox property, and assert that a TypeError is thrown. Add a matching sloppy-mode test that asserts the write is silently ignored. This pins the behavior of the new parameter at both ends.

Do you want me to draft these tests?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/tests.rs` around lines 1000 - 1012, Add two
new tests in the tests.rs file to cover strict-mode object-environment writes
with the new strict flag threaded through env::assign, object_write_binding, and
bridge::set_index. First, create a test that evaluates a "use strict" script
attempting to assign to a non-writable or frozen sandbox property and asserts
that a TypeError is thrown. Second, add a corresponding sloppy-mode test using
the same assignment target that asserts the write is silently ignored instead,
verifying the behavior difference between the two modes.
crates/perry-runtime/src/dyn_eval/expr.rs (1)

129-137: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Remove the duplicated global lookup.

global_has_own calls bridge::global_has_property, then calls bridge::global_lookup, which calls global_has_property again internally. Each call unwraps the proxy and performs a property lookup.

eval_ident already computed global_lookup before reaching line 123, and eval_unary calls global_has_own for every unresolved typeof operand. Pass the already-computed value instead of recomputing it.

♻️ Proposed refactor
-fn global_has_own(ctx: &Ctx, name: &str) -> bool {
-    let global = root_get(ctx.global_idx);
-    bridge::global_has_property(global, name)
-        || !bridge::is_undefined(bridge::global_lookup(
-            global,
-            root_get(ctx.intrinsics_idx),
-            name,
-        ))
+fn global_has_own(ctx: &Ctx, name: &str) -> bool {
+    let global = root_get(ctx.global_idx);
+    if bridge::global_has_property(global, name) {
+        return true;
+    }
+    !bridge::is_undefined(bridge::global_lookup(
+        global,
+        root_get(ctx.intrinsics_idx),
+        name,
+    ))
 }

eval_ident can then skip the second probe entirely, because a non-undefined global_lookup result already returned at line 121.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/expr.rs` around lines 129 - 137, The
global_has_own function performs redundant property lookups by calling
bridge::global_lookup internally, which already calls
bridge::global_has_property. Since eval_ident and eval_unary have already
computed the global_lookup result before calling global_has_own, refactor the
function to accept the pre-computed lookup result as a parameter instead of
recomputing it. Update global_has_own to use the passed-in lookup value to
determine if the property exists globally, and update all callers (eval_ident
and eval_unary) to pass the already-computed global_lookup result. This
eliminates the duplicate bridge::global_has_property and bridge::global_lookup
calls within global_has_own.
crates/perry-runtime/src/node_vm.rs (1)

1098-1112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that options.timeout is validated but not enforced.

validate_run_options validates timeout, displayErrors, and breakOnSigint, and execute_in_state ignores all three. A caller that passes timeout to bound runaway script code receives no time bound. Add a short comment that records the gap, so a later reader does not assume enforcement exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm.rs` around lines 1098 - 1112, Add a short
comment near validate_run_options or the relevant execute_in_state call
documenting that options.timeout, displayErrors, and breakOnSigint are validated
but not enforced during execution, so timeout does not bound runaway scripts.
crates/perry-codegen/src/lower_call/native_table/node_misc.rs (1)

179-190: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale comment above the vm.createContext row.

The comment states that the surface covers "APIs that require a vm context object but do not execute code inside it yet". This PR makes contexts execute code, so the comment now contradicts the behavior. The signature change itself matches js_vm_create_context(sandbox: f64, options: f64) and the &[DOUBLE, DOUBLE] declaration in crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/lower_call/native_table/node_misc.rs` around lines
179 - 190, Update the comment above the `NativeModSig` entry for
`vm.createContext` to reflect that contexts now execute code, removing the stale
statement that they do not execute code inside them. Leave the signature and
runtime mapping unchanged.
crates/perry-runtime/src/value/to_string.rs (1)

1022-1025: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared "VM compiled source, else func_ptr source" fallback. The same three-step chain is now copied into three files: look up node_vm::compiled_function_source_for_closure(addr), otherwise read func_ptr from the ClosureHeader and call builtins::function_source_for_func_ptr. Each copy is correct, but a future change to the precedence must be applied three times, and the copies can drift.

Add one helper, for example crate::builtins::function_source_for_closure_addr(addr: usize) -> String, and call it from all three sites.

  • crates/perry-runtime/src/value/to_string.rs#L1022-L1025: replace the lookup and the following func_ptr fallback with the shared helper.
  • crates/perry-runtime/src/object/global_this/array_error.rs#L567-L571: replace the unwrap_or_else chain with the shared helper.
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs#L125-L131: replace the unwrap_or_else chain with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/value/to_string.rs` around lines 1022 - 1025,
Extract the duplicated closure-source precedence into
builtins::function_source_for_closure_addr(addr: usize) -> String, performing
the compiled-function lookup followed by the ClosureHeader func_ptr fallback.
Replace the existing chains at
crates/perry-runtime/src/value/to_string.rs:1022-1025,
crates/perry-runtime/src/object/global_this/array_error.rs:567-571, and
crates/perry-runtime/src/object/native_call_method/primitive_methods.rs:125-131
with calls to this helper.
crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs (1)

160-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the createScript branding decision.

Branding now lives in two places. The codegen native table routes createScript to js_vm_create_script_branded, which brands internally, and this dispatcher applies brand_vm_script_instance again on its own path. A future VM method that needs branding must be changed in both files, and the two lists can drift.

Move the branding into crate::node_vm::dispatch_vm_method for the "createScript" arm, and let this dispatcher return dispatch_vm_method unchanged. js_vm_create_script_branded can then call the same branded helper.

♻️ Proposed simplification
-        ("vm", m) => {
-            let value = crate::node_vm::dispatch_vm_method(m, arg(0), arg(1), arg(2));
-            if m == "createScript" {
-                crate::object::class_registry::brand_vm_script_instance(value)
-            } else {
-                value
-            }
-        }
+        ("vm", m) => crate::node_vm::dispatch_vm_method(m, arg(0), arg(1), arg(2)),

Then brand inside dispatch_vm_method's "createScript" arm.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs`
around lines 160 - 167, Move the createScript branding logic into the
"createScript" arm of crate::node_vm::dispatch_vm_method, ensuring it returns
the branded VM script instance. Update the "vm" branch here to return
dispatch_vm_method’s result unchanged, removing the local
brand_vm_script_instance call while preserving other method behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs`:
- Line 313: Add an LLVM declaration entry alongside the existing native runtime
declarations in inspector_vm.rs for the js_vm_create_script_branded runtime
symbol, matching its external C signature with a DOUBLE return type and two
DOUBLE parameters so the emitted call has a declared FFI import.

In `@crates/perry-runtime/src/dyn_eval/expr.rs`:
- Around line 722-736: Update the direct-eval handling in eval_call to recreate
the caller’s call context when invoking eval_script_in, using the appropriate
fresh variable environment so strict and sloppy eval declarations follow the
correct lexical and var-binding rules. Also enforce the
context.codeGeneration.strings permission before evaluating the source, ensuring
generated-string paths such as compileFunction cannot bypass the
disabled-strings check.

In `@crates/perry-runtime/src/dyn_eval/mod.rs`:
- Around line 292-318: Correct the documentation above script_environment so it
states that the last object_envs entry has the highest lookup precedence,
matching object_environment_chain and the existing function_from_strings_in
description; leave the implementation unchanged.

In `@crates/perry-runtime/src/node_vm.rs`:
- Around line 1341-1348: Update execute_in_state and the dyn_eval evaluation
path to pass strings_allowed into dyn_eval, enforcing the restriction at the
eval/Function intrinsic entry points instead of scanning source text; remove the
substring-based rejection so strings in comments or literals remain valid. Also
propagate state.wasm_allowed through the execution path and enforce the wasm
restriction where wasm evaluation is initiated, rather than assigning it to the
unused _wasm_allowed variable.
- Around line 1896-1908: Update the documentation above
prune_dead_vm_owner_entries to reflect that VM_CONTEXTS is thread-local and only
the calling thread’s contexts are pruned. State that foreign-thread VM_CONTEXTS
entries are unreachable from this call, while the process-global residual
applies only to VM_SCRIPTS and VM_COMPILED_FUNCTION_SOURCES.
- Around line 940-970: Update install_module_accessor after storing the module
in capture slot 0 to register the closure’s one-slot capture layout and trigger
the post-store GC barrier, using the existing capture-layout helper or
rebuild_closure_layout_and_barriers with closure and 1. Ensure the captured
module pointer is traceable and rewriteable before the accessor closure is
returned.
- Around line 809-820: Update the statement processing in the filter_map chain
to strip only the `export ` prefix (preserving declaration keywords like
`const`, `let`, `var`, `function`, `class`) instead of stripping the full
declaration keyword prefixes. After evaluating the resulting executable
statements, read the exported declarations from the evaluated environment
context and populate them into the namespace so that all export forms (export
const, export function, export class, export { ... }) are properly available.
- Around line 59-65: The thread-local MAIN_CONTEXT is shared across different
execution contexts while scan_vm_roots_mut is registered only per-thread,
creating a mismatch that can leave stale raw pointers after GC relocations.
Ensure MAIN_CONTEXT is properly handled in the GC root scanning path and
includes appropriate relocation or forwarding logic when accessed from different
threads. Add integration tests that verify VM contexts survive minor copy/move
GC operations and add tests that re-root contexts across different threads to
confirm no stale pointers remain after relocation.

In `@crates/perry-runtime/src/object/class_registry/construct.rs`:
- Around line 163-173: Update the pointer-validation guard surrounding the
unsafe class_id assignment to also verify the allocation’s GC type is
GC_TYPE_OBJECT, matching the established check in instanceof. Only write
(*object).class_id after confirming the pointer is non-null, valid, above the
handle band, and specifically an object allocation; preserve all existing checks
and behavior otherwise.

In `@crates/perry-runtime/src/object/instanceof.rs`:
- Around line 74-81: Update recorded_prototype_instanceof_builtin to root value
with crate::gc::RuntimeHandleScope before calling
js_get_global_this_builtin_value, then reload the rooted value for
ordinary_has_instance_prototype_walk after lookup. Validate that the resolved
constructor is an object; return None when it is unresolved or non-object
instead of committing Some(false), while preserving the existing prototype-walk
result for valid constructors.

---

Outside diff comments:
In `@crates/perry-runtime/src/dyn_eval/env.rs`:
- Around line 279-317: Reload GC-managed values from rooted storage after any
allocating call instead of reusing stale Rust locals: in
crates/perry-runtime/src/dyn_eval/env.rs:279-317, remove current and use
root_get(cur_idx) at the indicated accesses; in
crates/perry-runtime/src/dyn_eval/env.rs:398-411, re-derive
env_object_bindings(root_get(cur_idx)) before each object_write_binding call,
while object_has_binding may allocate; in
crates/perry-runtime/src/object/class_registry/construct.rs:153-162, remove
constructor_value and pass constructor.get_nanbox_f64() directly to
synthetic_class_id_for_function and install_script_prototypes.

In `@crates/perry-runtime/src/node_vm.rs`:
- Around line 1496-1524: Root the object returned by js_object_alloc in the
node_vm.rs metadata-cache path at lines 1496-1524 with RuntimeHandleScope,
reload the handle for the metadata-map key and every set_field receiver, and
construct the returned value only after the final allocating write. In the
sibling node_vm.rs path at lines 1739-1757, root the with_source_location result
and reload it before set_builtin_closure_length, the compiled_function_sources
insert, and each set_value_field call; preserve these roots across every
possible collection point.

In `@crates/perry-runtime/src/object/descriptors.rs`:
- Around line 1627-1632: Update the configurable argument in the
build_data_descriptor call for the vm namespace tail case so properties are
configurable for vm and non-vm modules, but remain non-configurable when
module_name is exactly "vm.constants". Preserve the separate fs.constants
handling above.

---

Nitpick comments:
In `@crates/perry-codegen/src/lower_call/native_table/node_misc.rs`:
- Around line 179-190: Update the comment above the `NativeModSig` entry for
`vm.createContext` to reflect that contexts now execute code, removing the stale
statement that they do not execute code inside them. Leave the signature and
runtime mapping unchanged.

In `@crates/perry-runtime/src/dyn_eval/expr.rs`:
- Around line 129-137: The global_has_own function performs redundant property
lookups by calling bridge::global_lookup internally, which already calls
bridge::global_has_property. Since eval_ident and eval_unary have already
computed the global_lookup result before calling global_has_own, refactor the
function to accept the pre-computed lookup result as a parameter instead of
recomputing it. Update global_has_own to use the passed-in lookup value to
determine if the property exists globally, and update all callers (eval_ident
and eval_unary) to pass the already-computed global_lookup result. This
eliminates the duplicate bridge::global_has_property and bridge::global_lookup
calls within global_has_own.

In `@crates/perry-runtime/src/dyn_eval/tests.rs`:
- Around line 993-997: The js_instanceof call in the assertion hardcodes the
Promise class id as 0xFFFF_0027, but the constant CLASS_ID_PROMISE is already
defined in crate::object and should be referenced instead. Replace the hardcoded
literal 0xFFFF_0027 with crate::object::CLASS_ID_PROMISE to ensure the test
automatically stays in sync if the class id constant is updated.
- Around line 1000-1012: Add two new tests in the tests.rs file to cover
strict-mode object-environment writes with the new strict flag threaded through
env::assign, object_write_binding, and bridge::set_index. First, create a test
that evaluates a "use strict" script attempting to assign to a non-writable or
frozen sandbox property and asserts that a TypeError is thrown. Second, add a
corresponding sloppy-mode test using the same assignment target that asserts the
write is silently ignored instead, verifying the behavior difference between the
two modes.

In `@crates/perry-runtime/src/node_vm.rs`:
- Around line 1098-1112: Add a short comment near validate_run_options or the
relevant execute_in_state call documenting that options.timeout, displayErrors,
and breakOnSigint are validated but not enforced during execution, so timeout
does not bound runaway scripts.

In `@crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs`:
- Around line 160-167: Move the createScript branding logic into the
"createScript" arm of crate::node_vm::dispatch_vm_method, ensuring it returns
the branded VM script instance. Update the "vm" branch here to return
dispatch_vm_method’s result unchanged, removing the local
brand_vm_script_instance call while preserving other method behavior.

In `@crates/perry-runtime/src/value/to_string.rs`:
- Around line 1022-1025: Extract the duplicated closure-source precedence into
builtins::function_source_for_closure_addr(addr: usize) -> String, performing
the compiled-function lookup followed by the ClosureHeader func_ptr fallback.
Replace the existing chains at
crates/perry-runtime/src/value/to_string.rs:1022-1025,
crates/perry-runtime/src/object/global_this/array_error.rs:567-571, and
crates/perry-runtime/src/object/native_call_method/primitive_methods.rs:125-131
with calls to this helper.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf942bb2-3c5c-42cc-983a-c0dd1c5d05c8

📥 Commits

Reviewing files that changed from the base of the PR and between 37cd06d and dc077c3.

📒 Files selected for processing (24)
  • changelog.d/7382-node-vm-node26-parity.md
  • crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs
  • crates/perry-hir/src/lower/lower_expr/arm_bin.rs
  • crates/perry-runtime/src/dyn_eval/bridge.rs
  • crates/perry-runtime/src/dyn_eval/env.rs
  • crates/perry-runtime/src/dyn_eval/expr.rs
  • crates/perry-runtime/src/dyn_eval/interp.rs
  • crates/perry-runtime/src/dyn_eval/mod.rs
  • crates/perry-runtime/src/dyn_eval/tests.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/node_submodules/mod.rs
  • crates/perry-runtime/src/node_vm.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_module/namespace_builders.rs
  • crates/perry-runtime/src/object/native_module_dispatch/dispatch_v_z.rs
  • crates/perry-runtime/src/value/to_string.rs

Comment thread crates/perry-runtime/src/dyn_eval/mod.rs
Comment thread crates/perry-runtime/src/node_vm.rs
Comment thread crates/perry-runtime/src/node_vm.rs Outdated
Comment thread crates/perry-runtime/src/node_vm.rs Outdated
Comment thread crates/perry-runtime/src/node_vm.rs Outdated
Comment thread crates/perry-runtime/src/node_vm.rs Outdated
Comment thread crates/perry-runtime/src/object/class_registry/construct.rs Outdated
Comment thread crates/perry-runtime/src/object/instanceof.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/perry-runtime/src/dyn_eval/interp.rs (1)

357-370: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Don’t bind arguments inside arrow call frames.

The interpreter creates an arguments object and defines it for every invoke_interp_fn frame, including arrow functions. make_function_value already tracks is_arrow, so add an arrow flag to InterpFn or pass the arrow context to invoke_interp_fn, then omit the arguments definition and let arrows inherit it from the enclosing scope.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/interp.rs` around lines 357 - 370, Update
the invoke_interp_fn frame setup to know whether the target function is an
arrow, using the existing make_function_value is_arrow state or an equivalent
InterpFn field. Only create and env::define the arguments object for non-arrow
functions; arrow frames must omit their own binding so scope lookup inherits the
enclosing arguments.
crates/perry-runtime/src/dyn_eval/env.rs (1)

445-457: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Throw ReferenceError for strict unresolvable assignments before creating a binding.

In strict mode, assign() must not continue to object_write_binding() or env_write() after reaching the root with no present binding. js_put_value_set() only rejects existing non-writable/locked properties and can create a miss; env_write() creates the own binding unconditionally. Throw via super::bridge::throw_reference_error(...) on the unresolvable root path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/env.rs` around lines 445 - 457, Update the
unresolvable root branch in assign() so strict assignments throw ReferenceError
via super::bridge::throw_reference_error(...) before object_write_binding() or
env_write() can create a binding. Preserve the existing binding writes for
non-strict assignments and truncate roots on the handled return path.
🧹 Nitpick comments (1)
crates/perry-runtime/src/dyn_eval/mod.rs (1)

410-423: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compute the variable environment index once.

Lines 410-414 and Lines 419-423 contain the same conditional. The two expressions must stay in sync; a future edit to one changes the eval scoping semantics without changing the other.

Bind the index once and use it in both places.

♻️ Proposed refactor
     let ret_idx = root_push(bridge::undefined());
+    // Strict direct eval owns a fresh variable environment; sloppy direct
+    // eval publishes `var` bindings in the caller's variable environment.
+    let variable_env_idx = if strict {
+        lexical_env_idx
+    } else {
+        caller_variable_env_idx
+    };
     let ctx = interp::Ctx {
         this_idx: global_idx,
         ret_idx,
         global_idx,
         intrinsics_idx,
-        variable_env_idx: if strict {
-            lexical_env_idx
-        } else {
-            caller_variable_env_idx
-        },
+        variable_env_idx,
         strict,
         strings_allowed,
         wasm_allowed,
     };
-    let variable_env_idx = if strict {
-        lexical_env_idx
-    } else {
-        caller_variable_env_idx
-    };
     let _ = interp::exec_direct_eval_stmts(&ctx, &statements, lexical_env_idx, variable_env_idx);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/dyn_eval/mod.rs` around lines 410 - 423, In the
surrounding dynamic evaluation setup, compute the strict-versus-nonstrict
environment index once and bind it before constructing the environment
configuration. Reuse that binding for both the `variable_env_idx` field and the
later local variable, removing the duplicated conditional while preserving the
existing strict scoping behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/dyn_eval/expr.rs`:
- Around line 854-861: Update the blocked WebAssembly rejection paths in the
expression evaluation logic, including both occurrences near the existing
checks, to call roots_truncate(obj_idx) before returning
bridge::wasm_codegen_rejection. Preserve the current rejection behavior and the
instantiate exception while ensuring root slots are truncated on every early
return.
- Around line 800-806: Update both member-call paths in
crates/perry-runtime/src/dyn_eval/expr.rs at lines 800-806 and 836-840: resolve
each callee once into a rooted slot using the existing member/index lookup,
classify that resolved value, and dispatch it directly instead of re-reading it
through call_method at line 828 or call_method_value at line 863. Preserve
single getter evaluation and existing codegen-blocking behavior in both paths.
- Around line 749-779: Update the direct-eval success path in the `Ident`/`eval`
handling to store the result of `super::eval_direct_in` before returning,
truncate the root stack using `resolved_idx`, then return the stored result.
Preserve the existing argument evaluation and error behavior while ensuring
every resolved eval slot is released before either return path.

In `@crates/perry-runtime/src/dyn_eval/interp.rs`:
- Around line 553-556: Update the hoist_fn_decls call in the eval setup to pass
ctx.strict as its declaration-mode argument instead of false, ensuring strict
direct eval function declarations are created in the fresh lexical environment
rather than leaking to the root environment.

In `@crates/perry-runtime/src/dyn_eval/mod.rs`:
- Around line 328-339: In crates/perry-runtime/src/dyn_eval/mod.rs lines
328-339, root_push global_this and intrinsics before calling
prepare_function_args, then use root_get values when constructing the closure
via alloc_interp_closure. In crates/perry-runtime/src/dyn_eval/mod.rs lines
396-402, move the root_push calls for global_this, intrinsics, caller_env, and
caller_variable_env above parse_script_statements, and use the rooted values
afterward so all GC-managed parameters remain valid across allocation points.

---

Outside diff comments:
In `@crates/perry-runtime/src/dyn_eval/env.rs`:
- Around line 445-457: Update the unresolvable root branch in assign() so strict
assignments throw ReferenceError via super::bridge::throw_reference_error(...)
before object_write_binding() or env_write() can create a binding. Preserve the
existing binding writes for non-strict assignments and truncate roots on the
handled return path.

In `@crates/perry-runtime/src/dyn_eval/interp.rs`:
- Around line 357-370: Update the invoke_interp_fn frame setup to know whether
the target function is an arrow, using the existing make_function_value is_arrow
state or an equivalent InterpFn field. Only create and env::define the arguments
object for non-arrow functions; arrow frames must omit their own binding so
scope lookup inherits the enclosing arguments.

---

Nitpick comments:
In `@crates/perry-runtime/src/dyn_eval/mod.rs`:
- Around line 410-423: In the surrounding dynamic evaluation setup, compute the
strict-versus-nonstrict environment index once and bind it before constructing
the environment configuration. Reuse that binding for both the
`variable_env_idx` field and the later local variable, removing the duplicated
conditional while preserving the existing strict scoping behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cf04f90d-1491-4fa0-bd22-7f379dbc15f0

📥 Commits

Reviewing files that changed from the base of the PR and between dc077c3 and f144d75.

📒 Files selected for processing (16)
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs
  • crates/perry-runtime/src/dyn_eval/bridge.rs
  • crates/perry-runtime/src/dyn_eval/env.rs
  • crates/perry-runtime/src/dyn_eval/expr.rs
  • crates/perry-runtime/src/dyn_eval/interp.rs
  • crates/perry-runtime/src/dyn_eval/mod.rs
  • crates/perry-runtime/src/node_vm.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/global_this_webassembly.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/value/to_string.rs
💤 Files with no reviewable changes (1)
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • crates/perry-runtime/src/value/to_string.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/dyn_eval/bridge.rs
  • crates/perry-runtime/src/node_vm.rs

Comment thread crates/perry-runtime/src/dyn_eval/expr.rs
Comment thread crates/perry-runtime/src/dyn_eval/expr.rs Outdated
Comment thread crates/perry-runtime/src/dyn_eval/expr.rs
Comment thread crates/perry-runtime/src/dyn_eval/interp.rs Outdated
Comment thread crates/perry-runtime/src/dyn_eval/mod.rs
TheHypnoo and others added 5 commits August 7, 2026 17:53
Rebase-ready follow-up for #7382:

- Feature-gate dyn-eval/perry-parser use in node_vm so product builds
  (perry → runtime with default-features=false) compile.
- Route bare RuntimeHandle reads through across_* so raw-handle debt
  stays at baseline (new modules must stay at zero).
- Split module lifecycle into node_vm/modules.rs and move Script
  branding into class_registry/vm_brand.rs to clear the 2000-line gate.
- Drop the orphaned mini-interpreter eval.rs replaced by dyn_eval.
@proggeramlug
proggeramlug force-pushed the fix/node-vm-node26-parity branch from a50c275 to 91a1c5c Compare August 7, 2026 16:07
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (1)
crates/perry-runtime/src/object/class_registry/vm_brand.rs (1)

16-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or document the discarded ordinary_function_prototype_value_for_read call.

Line 16 discards the result. install_script_prototypes in crates/perry-runtime/src/node_vm.rs (lines 1302-1310) already calls ordinary_function_prototype_value_for_read on the same constructor as its first step. Line 16 is therefore redundant unless it exists to force lazy prototype materialization before line 17.

If the call has a required side effect, add a short comment that states it. Otherwise remove the line.

♻️ Proposed cleanup
-    let _ = ordinary_function_prototype_value_for_read(constructor.get_nanbox_f64());
     crate::node_vm::install_script_prototypes(constructor.get_nanbox_f64());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/class_registry/vm_brand.rs` around lines 16 -
17, Remove the discarded ordinary_function_prototype_value_for_read call in the
surrounding initialization flow because install_script_prototypes already
performs it for the same constructor. If the call is required to force lazy
prototype materialization before install_script_prototypes, retain it and add a
brief comment documenting that side effect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/node_vm/modules.rs`:
- Around line 447-459: Update js_vm_source_text_module_link_requests to validate
every entry in modules as a source-text module before storing the array,
rejecting non-module values with the existing VM error mechanism. After
successful validation, transition the module to STATUS_LINKED so linkRequests
establishes the same linked state expected by module_has_async_graph and
downstream lifecycle handling.
- Around line 383-398: In crates/perry-runtime/src/node_vm/modules.rs at lines
383-398, update js_vm_module_link to open a RuntimeHandleScope, root module,
module_value, linker, and linked, and reload them via hmut::<...> or
get_nanbox_f64() after each allocating or user-JS call, including after
js_array_push_f64. In lines 59-78, update evaluate_synthetic_module to root
module, callback, and prev and reload module before set_status; in lines
401-416, update js_vm_module_evaluate to root module and result and reload them
before status and promise operations; in lines 488-507, update
js_vm_synthetic_module_set_export to root value and reload it before set_field.
- Around line 365-367: Update the namespace status guard in the module namespace
access path to reject both STATUS_UNLINKED and STATUS_LINKING for source
modules, while preserving the existing throw_vm_status behavior.
- Around line 202-204: Update the module construction flow around
validate_module_source to inspect its boolean result and reject invalid source
before calling module_options or constructing the module. Use the existing
syntax-error helper from node_vm.rs, replacing throw_vm_syntax_error as
indicated, while preserving successful handling for valid source.
- Around line 87-104: Add cycle detection to module_has_async_graph by
introducing a visited set keyed by module pointers and threading it through the
recursive traversal. Mark each module before checking its TLA or linked modules,
and return false when a module has already been visited; preserve the existing
true result when any newly visited module has TLA.
- Around line 6-57: Update evaluate_source_module so pre-evaluation failures use
the same returned-error path as failures caught from eval_script_in, allowing
js_vm_module_evaluate to reject the promise rather than synchronously throw.
Replace the throw_vm_status handling for invalid module status and unavailable
namespace with an error value, store it in FIELD_ERROR, mark the module
STATUS_ERRORED, and return it; preserve the existing evaluated-module fast path
and successful evaluation behavior.
- Around line 71-75: Update the synthetic callback flow around
js_native_call_value to invoke it through crate::exception::js_call_catching,
ensuring the previous implicit-this value is restored on both success and error
paths. In the error branch, transition the module from STATUS_EVALUATING to
STATUS_ERRORED and propagate the caught exception so module evaluation rejects
correctly.
- Around line 488-507: Update js_vm_synthetic_module_set_export to root the
incoming value immediately on entry and reload the rooted value before
set_field, preserving it across read_exports and namespace_for_module. Also
validate module_kind before processing exports and reject non-synthetic modules,
ensuring setExport only succeeds for SyntheticModule receivers.
- Around line 158-166: Update the PropertyAttrs::new call in the module accessor
descriptor setup to keep writable and enumerable false while setting
configurable true, allowing the namespace and error accessors to be redefined.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/class_registry/vm_brand.rs`:
- Around line 16-17: Remove the discarded
ordinary_function_prototype_value_for_read call in the surrounding
initialization flow because install_script_prototypes already performs it for
the same constructor. If the call is required to force lazy prototype
materialization before install_script_prototypes, retain it and add a brief
comment documenting that side effect.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1100b041-871b-4c47-ab16-f90d42bbe1f9

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb58cd and 91a1c5c.

📒 Files selected for processing (28)
  • changelog.d/7382-node-vm-node26-parity.md
  • crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs
  • crates/perry-hir/src/lower/lower_expr/arm_bin.rs
  • crates/perry-runtime/src/dyn_eval/bridge.rs
  • crates/perry-runtime/src/dyn_eval/env.rs
  • crates/perry-runtime/src/dyn_eval/expr.rs
  • crates/perry-runtime/src/dyn_eval/interp.rs
  • crates/perry-runtime/src/dyn_eval/mod.rs
  • crates/perry-runtime/src/dyn_eval/tests.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/node_submodules/mod.rs
  • crates/perry-runtime/src/node_vm.rs
  • crates/perry-runtime/src/node_vm/eval.rs
  • crates/perry-runtime/src/node_vm/modules.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/class_registry/vm_brand.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/global_this_webassembly.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_module/namespace_builders.rs
  • crates/perry-runtime/src/value/to_string.rs
💤 Files with no reviewable changes (1)
  • crates/perry-runtime/src/node_vm/eval.rs
🚧 Files skipped from review as they are similar to previous changes (24)
  • changelog.d/7382-node-vm-node26-parity.md
  • crates/perry-codegen/src/lower_call/native_table/node_misc.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_module/namespace_builders.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi/net_http.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/value/to_string.rs
  • crates/perry-hir/src/lower/lower_expr/arm_bin.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-codegen/src/lower_call/native_table/node_core/inspector_vm.rs
  • crates/perry-runtime/src/object/class_registry/construct.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/global_this_webassembly.rs
  • crates/perry-runtime/src/node_submodules/mod.rs
  • crates/perry-runtime/src/dyn_eval/env.rs
  • crates/perry-runtime/src/dyn_eval/bridge.rs
  • crates/perry-runtime/src/dyn_eval/interp.rs
  • crates/perry-runtime/src/node_vm.rs
  • crates/perry-runtime/src/dyn_eval/tests.rs
  • crates/perry-runtime/src/dyn_eval/expr.rs
  • crates/perry-runtime/src/dyn_eval/mod.rs

Comment on lines +6 to +57
fn evaluate_source_module(module: *mut ObjectHeader) -> f64 {
let scope = crate::gc::RuntimeHandleScope::new();
let module = scope.root_raw_mut_ptr(module);
let status = module_status(hmut::<ObjectHeader>(&module));
if status != STATUS_LINKED && status != STATUS_EVALUATED {
return throw_vm_status("Module status must be linked");
}
if status == STATUS_EVALUATED {
return undefined_value();
}

set_status(hmut::<ObjectHeader>(&module), STATUS_EVALUATING);
let Some(namespace) = namespace_for_module(hmut::<ObjectHeader>(&module)) else {
set_status(hmut::<ObjectHeader>(&module), STATUS_ERRORED);
return throw_vm_status("Module namespace is unavailable");
};
let namespace = scope.root_raw_mut_ptr(namespace);

let source = get_string_field(hmut::<ObjectHeader>(&module), FIELD_SOURCE).unwrap_or_default();
let context = scope.root_nanbox_f64(get_field(hmut::<ObjectHeader>(&module), FIELD_CONTEXT));
for (name, value) in build_import_env(hmut::<ObjectHeader>(&module)) {
set_object_field(context.get_nanbox_f64(), &name, value);
}
let executable = split_source_statements(&source)
.into_iter()
.filter(|stmt| !stmt.starts_with("import "))
.map(|stmt| stmt.strip_prefix("export ").unwrap_or(&stmt).to_string())
.collect::<Vec<_>>()
.join(";");
let lexical = scope.root_nanbox_f64(de::script_environment(context.get_nanbox_f64(), &[]));
if let Err(error) = crate::exception::js_call_catching(|| {
de::eval_script_in(
&executable,
context.get_nanbox_f64(),
context.get_nanbox_f64(),
lexical.get_nanbox_f64(),
)
}) {
set_field(hmut::<ObjectHeader>(&module), FIELD_ERROR, error);
set_status(hmut::<ObjectHeader>(&module), STATUS_ERRORED);
return error;
}
for export in read_exports(hmut::<ObjectHeader>(&module)) {
set_field(
hmut::<ObjectHeader>(&namespace),
&export.name,
de::script_binding(lexical.get_nanbox_f64(), &export.name),
);
}
set_status(hmut::<ObjectHeader>(&module), STATUS_EVALUATED);
undefined_value()
}

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align failure reporting for evaluate() between the throw path and the reject path.

evaluate_source_module reports failures in two different ways. Lines 11 and 20 call throw_vm_status, which unwinds and never returns. Line 46 returns the caught error value, and js_vm_module_evaluate converts it into a rejected promise. So a status error propagates as a synchronous throw, while an evaluation error propagates as a rejected promise. Node rejects the returned promise in both cases.

The namespace-unavailable branch at lines 19-20 also sets STATUS_ERRORED without writing FIELD_ERROR. If that path ever becomes non-throwing, js_vm_module_error returns undefined for an errored module.

🐛 Proposed change to keep the status error inside the promise result
-    let Some(namespace) = namespace_for_module(hmut::<ObjectHeader>(&module)) else {
-        set_status(hmut::<ObjectHeader>(&module), STATUS_ERRORED);
-        return throw_vm_status("Module namespace is unavailable");
-    };
+    let Some(namespace) = namespace_for_module(hmut::<ObjectHeader>(&module)) else {
+        let error = crate::exception::js_call_catching(|| {
+            throw_vm_status("Module namespace is unavailable")
+        })
+        .err()
+        .unwrap_or_else(undefined_value);
+        set_field(hmut::<ObjectHeader>(&module), FIELD_ERROR, error);
+        set_status(hmut::<ObjectHeader>(&module), STATUS_ERRORED);
+        return error;
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm/modules.rs` around lines 6 - 57, Update
evaluate_source_module so pre-evaluation failures use the same returned-error
path as failures caught from eval_script_in, allowing js_vm_module_evaluate to
reject the promise rather than synchronously throw. Replace the throw_vm_status
handling for invalid module status and unavailable namespace with an error
value, store it in FIELD_ERROR, mark the module STATUS_ERRORED, and return it;
preserve the existing evaluated-module fast path and successful evaluation
behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred: aligning every pre-evaluation status throw to a rejected promise is a broader evaluate-path API change. The throw vs reject split for status errors remains; evaluation/callback failures already reject via STATUS_ERRORED. Happy to follow up if parity fixtures require the status-error path to reject instead of throw.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@proggeramlug Acknowledged. The current split remains intentional for this PR: pre-evaluation status errors throw, while evaluation and callback failures reject after STATUS_ERRORED.

Would you like me to create a follow-up GitHub issue for Node parity of vm.Module.prototype.evaluate() status errors?
🐇


✏️ Learnings added
Learnt from: proggeramlug
Repo: PerryTS/perry PR: 7382
File: crates/perry-runtime/src/node_vm/modules.rs:6-57
Timestamp: 2026-08-07T19:02:36.770Z
Learning: In `crates/perry-runtime/src/node_vm/modules.rs`, `vm.Module.prototype.evaluate()` currently throws for pre-evaluation status errors, while source-evaluation and callback failures record `STATUS_ERRORED` and reject through the returned-promise path. Aligning all pre-evaluation status errors to rejected promises is deferred as a broader evaluate-path API change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Comment thread crates/perry-runtime/src/node_vm/modules.rs
Comment thread crates/perry-runtime/src/node_vm/modules.rs
Comment on lines +158 to +166
crate::object::set_builtin_accessor_descriptor(
hmut::<ObjectHeader>(&module) as usize,
name.to_string(),
crate::object::AccessorDescriptor {
get: crate::value::js_nanbox_pointer(hmut::<ClosureHeader>(&closure) as i64).to_bits(),
set: 0,
},
PropertyAttrs::new(false, false, false),
);

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm PropertyAttrs::new signature and field order.
set -euo pipefail

ast-grep run --pattern 'pub fn new($$$) -> $_ { $$$ }' --lang rust crates/perry-runtime/src --json=stream \
  | jq -r 'select(.file | test("descriptor|attrs|propert"; "i")) | "\(.file):\(.range.start.line)\n\(.text)\n"'

rg -n -C6 'struct PropertyAttrs' crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 1839


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate modules.rs and relevant references =="
fd -a 'modules.rs|descriptor_state.rs|module|script' crates/perry-runtime/src | sed 's#^\./##' | head -100

echo
echo "== descriptor_state.rs relevant section =="
sed -n '1,90p' crates/perry-runtime/src/object/descriptor_state.rs

echo
echo "== modules.rs install_module_accessor and related accessors =="
rg -n -C8 'install_module_accessor|namespace|error|set_builtin_accessor_descriptor|PropertyAttrs::new|defineProperty|Reflect\.defineProperty' crates/perry-runtime/src/node_vm/modules.rs

echo
echo "== PropertyAttrs constructors/usages =="
rg -n 'PropertyAttrs::new\(|PropertyAttrs::default|bits' crates/perry-runtime/src/object/descriptor_state.rs crates/perry-runtime/src/node_vm/modules.rs

Repository: PerryTS/perry

Length of output: 22553


🌐 Web query:

Node.js Module prototype namespace error accessor configurable getter Object.getOwnPropertyDescriptor Module.prototype.namespace error

💡 Result:

In Node.js, attempting to access specific properties (such as 'namespace', 'identifier', 'context', 'status', or 'error') directly on Module.prototype or SourceTextModule.prototype will throw an error, typically a TypeError or a specific VM module error [1][2]. This behavior occurs because these properties are getters defined on the prototype that expect the 'this' context to be a valid instance of the corresponding module class [1][2]. When you access them on the prototype itself, the internal validation logic fails because the prototype is not an instance, triggering an exception [2]. If you are encountering errors related to Object.getOwnPropertyDescriptor or property assignment in tools like Jest, Bun, or dependency injection frameworks (e.g., attempting to overwrite or intercept prototype methods), it is often because you are attempting to modify or access read-only or internal properties that the Node.js runtime protects [3][4][5]. To avoid these errors: 1. Do not access these properties on the class prototype directly; only access them on instantiated module objects [2]. 2. If you are using frameworks that perform AOP (Aspect-Oriented Programming) or prototype interception, ensure they verify the descriptor before attempting to write to or replace a property [4]. Using Object.getOwnPropertyDescriptor and checking for writable: true before modification is a best practice to avoid "Attempted to assign to readonly property" errors [3][4]. 3. Be aware that Node.js internals are increasingly insulated, and modifying core prototypes like Module.prototype can lead to stability issues or runtime errors [5].

Citations:


🌐 Web query:

GitHub node source Module namespace SourceTextModule prototype configurable getter Object.defineProperty

💡 Result:

In the Node.js vm module, SourceTextModule instances include various getters (such as identifier, context, namespace, status, error, moduleRequests, and dependencySpecifiers) that are implemented to ensure they operate only on valid instances of the class [1][2]. These getters are defined on the SourceTextModule.prototype [1][2]. Because these properties are intended to be accessed only on valid instances, the implementation frequently uses Object.defineProperty to define them with internal logic that performs validation [3]. When a getter is accessed on an object that is not a valid instance (or a prototype), it typically throws an ERR_VM_MODULE_NOT_MODULE error [1][2]. The use of Object.defineProperty and similar mechanisms allows the Node.js vm implementation to enforce strict instance checks, protecting the internal state of modules—which are often backed by native C++ module records—from being accessed or manipulated incorrectly via the prototype chain [1][4]. These patterns help maintain security and stability within the vm module, preventing prototype pollution or misuse of the module interface [1][5].

Citations:


🌐 Web query:

"defineProperty" "SourceTextModule.prototype" "namespace" Node

💡 Result:

In Node.js, vm.SourceTextModule is a class that represents a Source Text Module Record, as defined in the ECMAScript specification [1][2][3]. It is part of the vm module, which allows for executing JavaScript code in V8 virtual machine contexts [1]. Regarding your query about defineProperty, SourceTextModule.prototype, and the namespace Node: 1. Namespace: In the context of Node.js API documentation, SourceTextModule is accessible via the vm module, typically imported as import vm from 'node:vm' [1]. 2. Prototype and defineProperty: The SourceTextModule.prototype object defines the methods and getters available to instances of SourceTextModule [4][5]. Developers often use Object.defineProperty or similar mechanisms in internal Node.js development or testing to manipulate, harden, or mock the behavior of these prototype methods [6][4][5]. For example, Node.js contributors have used tests to verify that getters on SourceTextModule.prototype (such as identifier, context, namespace, status, error, and dependencySpecifiers) correctly throw errors if invoked on a value that is not a valid instance of the module [4]. 3. namespace Property: There is a specific property named namespace on the SourceTextModule.prototype (and instances) [4]. This property returns the module namespace object of the evaluated module [1]. Attempting to access this getter on an object that is not a valid SourceTextModule instance will result in a TypeError (ERR_VM_MODULE_NOT_MODULE) [4]. If you are attempting to use Object.defineProperty on SourceTextModule.prototype to override or intercept its behavior, be aware that the internal implementation of these getters often includes strict checks to ensure the 'this' value is a valid instance of the class [5]. Directly modifying these prototypes is generally not recommended in production code, as it may interfere with the internal execution of the module system [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Node.js source lookup from local cache if present =="
find ~/.cache -maxdepth 4 -path '*node-*/src/js/*' -o -path '*node-*' -type f 2>/dev/null | rg 'src/js/module|src/js/source-text-module|vm/internal' | head -50 || true

echo
echo "== search any vendored/local Node sources =="
git ls-files | rg -i 'node|source-text|sourceTextModule|vm' | head -200

echo
echo "== deterministic descriptor parsing probe =="
python3 - <<'PY'
text = open('crates/perry-runtime/src/object/descriptor_state.rs').read()
start = text.index('pub const fn new(writable: bool, enumerable: bool, configurable: bool)')
end = text.index('}' , start)
print(text[start:end+1].splitlines()[-8:-2])
PY

echo
echo "== check current attrs usage =="
python3 - <<'PY'
import re
text = open('crates/perry-runtime/src/node_vm/modules.rs').read()
for i,line in enumerate(text.splitlines(),1):
    if 'PropertyAttrs::new' in line or 'install_module_accessor' in line or 'Module namespace' in line or 'sourceTextModule' in line.lower():
        print(f'{i}: {line}')
PY

Repository: PerryTS/perry

Length of output: 10570


🌐 Web query:

Node.js SourceTextModule prototype namespace descriptor configurable writable enumerable

💡 Result:

The vm.SourceTextModule class in Node.js represents a Source Text Module Record as defined in the ECMAScript specification [1][2]. Its namespace property provides access to the module's namespace object, which is an exotic object [1][3]. According to the ECMAScript specification (ECMA-262), Module Namespace Objects exhibit specific internal behaviors and property descriptors [4][5]: 1. Writable, Enumerable, and Configurable Attributes: For String-keyed properties (the exported bindings) of a Module Namespace Object, the property descriptors have the following attributes: - [[Writable]]: true - [[Enumerable]]: true - [[Configurable]]: false These properties are considered "live bindings," meaning their values reflect the current value of the binding in the module they belong to [4]. Because [[Configurable]] is false, these properties cannot be deleted or redefined, even though they are writable [4]. 2. Other Properties: The @@toStringTag property (representing the "Module" tag) is non-writable, non-enumerable, and non-configurable ({ [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }) [4]. The vm.SourceTextModule.namespace property itself is only available after the module has been successfully linked using module.link() [1][3]. Node.js implementation details in V8 (the engine behind Node.js) enforce these specification-defined constraints during internal operations such as DefineOwnProperty [6].

Citations:


🌐 Web query:

Node.js SourceTextModule.prototype namespace configurable Object.getOwnPropertyDescriptor SourceTextModule.prototype

💡 Result:

In Node.js, the SourceTextModule class is a component of the vm module used to represent ECMAScript modules. Accessing properties on SourceTextModule.prototype, such as via Object.getOwnPropertyDescriptor(SourceTextModule.prototype, 'propertyName'), is restricted to ensure these getters are only invoked on valid instances [1][2]. If you attempt to call a getter defined on SourceTextModule.prototype (like 'namespace', 'status', 'error', 'moduleRequests', or 'dependencySpecifiers') on an object that is not a proper instance of the module—for example, by using.call(undefined) or calling it on the prototype itself—Node.js explicitly throws a TypeError with the error code ERR_VM_MODULE_NOT_MODULE [1][2]. This is a security and stability hardening measure implemented to prevent invalid API usage [1]. Regarding the namespace and other properties: 1. Namespace: The namespace property is a getter that provides the module's namespace object, but it is only available after the module has successfully completed the linking process (module.link) [3][4]. 2. Hardened Checks: Node.js core development has specifically added checks to ensure that these prototype methods verify the this context. If the context is missing or invalid, the operation fails with the aforementioned error [1][2]. 3. Experimental Status: While the vm module and SourceTextModule are stable, certain related features or behaviors may have originated from experimental flags, and developers should consult the latest Node.js documentation for their specific version [5][3]. In summary, the SourceTextModule prototype properties are protected by internal type checks, and accessing them via property descriptors or prototype calls without a valid instance will trigger an error rather than returning the property value [1][2].

Citations:


🌐 Web query:

"Object.defineProperty" "SourceTextModule.prototype" "configurable" Node SourceTextModule namespace

💡 Result:

In the Node.js vm module, SourceTextModule.prototype properties and the module's namespace object are handled according to the ECMAScript module specification and Node.js internal implementation [1][2][3]. 1. SourceTextModule.namespace: The namespace property provides the module's namespace object [4][3]. It becomes available only after the module has been successfully linked (via linkRequests or similar mechanisms) [4][3]. It corresponds to the GetModuleNamespace abstract operation in the ECMAScript specification [4][3]. 2. Property Configuration: When manipulating objects within the context of a SourceTextModule—such as creating properties on import.meta or interacting with prototypes—care must be taken regarding the execution context [1][5]. Specifically, objects created in the top-level context (e.g., using {}) may behave differently than those created within a specific vm.createContext [1][6]. Developers are advised to use vm.runInContext('({})', contextifiedObject) to ensure objects are correctly associated with the target context [1][6]. 3. Recent API Changes: Node.js has been actively updating the SourceTextModule API to improve how it handles module requests and import attributes [7][8][9]. For example, the moduleRequests property has been updated to expose import attributes and import phases, while older properties like dependencySpecifiers have been deprecated [7][8][9]. These internal implementations often utilize Object.freeze or specific configuration settings to ensure the integrity of the module's interface [7]. If you are encountering issues with configurable properties or defining them on prototypes within these modules, ensure you are referencing the latest Node.js documentation for your specific version, as internal methods and getter behavior are subject to change to align with evolving ECMAScript standards [1][7][8].

Citations:


Make namespace and error configurable accessor descriptors.

PropertyAttrs::new(writable, enumerable, configurable) stores configurable in the third argument, so line 165 creates non-configurable accessors. These module accessors should not prevent redefinition; keep them non-writable and non-enumerable if that matches Node, but set configurable to true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm/modules.rs` around lines 158 - 166, Update
the PropertyAttrs::new call in the module accessor descriptor setup to keep
writable and enumerable false while setting configurable true, allowing the
namespace and error accessors to be redefined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change needed — PropertyAttrs::new(writable, enumerable, configurable) with (false, false, false) is the correct non-enumerable accessor shape for module getters.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🤖 web_search executed:

Node.js v26 source lib/internal/vm/module.js ObjectDefineProperties Module prototype namespace error configurable

Length of output: 9884


🏁 Script executed:

#!/bin/bash
set -euo pipefail

url='https://raw.githubusercontent.com/nodejs/node/v26.5.0/lib/internal/vm/module.js'
source="$(curl --fail --silent --show-error --location "$url")"

printf '%s\n' "$source" |
  rg -n -C8 'namespace|error|ObjectDefinePropert|ObjectDefineProperties|defineProperty' |
  head -240

Length of output: 7445


@proggeramlug, the finding remains valid.

Node.js v26.5.0 defines Module.prototype.namespace and Module.prototype.error with class getter syntax. Class getters create accessor descriptors with enumerable: false and configurable: true.

PropertyAttrs::new(false, false, false) correctly makes the accessors non-enumerable, but it also makes them non-configurable. The equivalent descriptor shape is:

PropertyAttrs::new(false, false, true)

writable does not apply to accessor descriptors.

You are interacting with an AI system.

Comment on lines +202 to +204
let source = code_string_required(code, "code");
validate_module_source(&source);
let (context, identifier) = module_options(options.get_nanbox_f64());

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle the validate_module_source result.

validate_module_source returns bool (see crates/perry-runtime/src/node_vm.rs lines 84-86). Line 203 discards it. Invalid module source is therefore accepted and new vm.SourceTextModule(...) succeeds. Node throws a SyntaxError at construction time. bool is not #[must_use], so the compiler does not warn about this.

🐛 Proposed fix to reject invalid source
     let source = code_string_required(code, "code");
-    validate_module_source(&source);
+    if !validate_module_source(&source) {
+        return throw_vm_syntax_error(&source);
+    }

Replace throw_vm_syntax_error with the existing syntax-error helper used elsewhere in node_vm.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm/modules.rs` around lines 202 - 204, Update
the module construction flow around validate_module_source to inspect its
boolean result and reject invalid source before calling module_options or
constructing the module. Use the existing syntax-error helper from node_vm.rs,
replacing throw_vm_syntax_error as indicated, while preserving successful
handling for valid source.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive — the parent validate_module_source is fn validate_module_source(source: &str) (unit return). On parse failure it calls throw_syntax, so the call site at construction already rejects invalid source the Node way. de::validate_module_source is the bool helper behind it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline crates/perry-runtime/src/node_vm.rs --items all --type function --match 'validate_module_source|throw_syntax'
printf '\n--- node_vm.rs relevant definitions ---\n'
rg -n -A10 -B5 '^\s*(pub\(crate\)\s+)?fn\s+(validate_module_source|throw_syntax)\b' crates/perry-runtime/src/node_vm.rs

printf '\n--- modules.rs constructor call site ---\n'
sed -n '195,215p' crates/perry-runtime/src/node_vm/modules.rs

printf '\n--- dynamic-eval helper, if present ---\n'
rg -n -A8 -B4 '^\s*(pub\(crate\)\s+)?fn\s+validate_module_source\b' crates/perry-runtime/src/dyn_eval

Length of output: 3224


@proggeramlug, confirmed. The parent validate_module_source(source: &str) returns () and checks de::validate_module_source(source). It calls throw_syntax("Invalid module source") when parsing fails.

The constructor call already rejects invalid module source with SyntaxError. The finding conflated the parent helper with de::validate_module_source. I will withdraw it.

🐇


✏️ Learnings added
Learnt from: proggeramlug
Repo: PerryTS/perry PR: 7382
File: crates/perry-runtime/src/node_vm/modules.rs:225-227
Timestamp: 2026-08-07T19:02:50.738Z
Learning: In `crates/perry-runtime/src/node_vm.rs`, the parent `validate_module_source(source: &str)` returns unit. It calls the boolean helper `de::validate_module_source(source)` and throws `SyntaxError` through `throw_syntax("Invalid module source")` when validation fails. Calls from `crates/perry-runtime/src/node_vm/modules.rs` therefore already reject invalid `vm.SourceTextModule` source during construction.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Comment thread crates/perry-runtime/src/node_vm/modules.rs Outdated
Comment thread crates/perry-runtime/src/node_vm/modules.rs Outdated
Comment on lines +447 to +459
pub extern "C" fn js_vm_source_text_module_link_requests(
module_value: f64,
modules_value: f64,
) -> f64 {
let Some(module) = object_ptr_from_value(module_value) else {
return undefined_value();
};
let Some(modules) = array_ptr_from_value(modules_value) else {
return throw_vm_type("linkRequests modules must be an array");
};
set_field(module, FIELD_LINKED_MODULES, array_value(modules));
undefined_value()
}

@coderabbitai coderabbitai Bot Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate the entries passed to linkRequests and set the linked status.

js_vm_source_text_module_link_requests stores modules_value directly into FIELD_LINKED_MODULES. It checks only that the value is an array. Any element type is accepted. module_has_async_graph later calls object_ptr_from_value on each element and recurses into plain objects as if they were modules.

The function also leaves the status unchanged. A caller that uses linkRequests instead of link must call instantiate separately to reach STATUS_LINKED.

Reject elements that are not modules, and confirm the intended status transition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm/modules.rs` around lines 447 - 459, Update
js_vm_source_text_module_link_requests to validate every entry in modules as a
source-text module before storing the array, rejecting non-module values with
the existing VM error mechanism. After successful validation, transition the
module to STATUS_LINKED so linkRequests establishes the same linked state
expected by module_has_async_graph and downstream lifecycle handling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred for a follow-up: validating every linkRequests element as a module and forcing a status transition needs a clear Node oracle for the experimental API surface. hasAsyncGraph now skips non-module elements safely via object_ptr_from_value and cycle detection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="crates/perry-runtime/src/node_vm/modules.rs"

ast-grep outline "$file" --items all --type function --match 'module_has_async_graph|js_vm_source_text_module_link_requests|object_ptr_from_value|module_kind'

rg -n -A45 -B8 \
  'fn module_has_async_graph\b|pub extern "C" fn js_vm_source_text_module_link_requests\b|fn object_ptr_from_value\b|fn module_kind\b' \
  "$file"

Length of output: 4867


@proggeramlug, verified. module_has_async_graph now uses a visited-pointer set, so cyclic linked-module graphs terminate.

linkRequests still accepts every array element. object_ptr_from_value filters primitives, but it accepts plain objects. The traversal currently treats such objects as nodes with no module fields.

I will leave the module validation and status-transition question deferred pending a Node oracle for this experimental API. Would you like me to create a GitHub follow-up issue for it?

You are interacting with an AI system.

Comment thread crates/perry-runtime/src/node_vm/modules.rs
Address CodeRabbit findings on the node:vm module path:

- Catch synthetic evaluate callbacks so implicit-this is restored and
  the module is marked errored instead of stuck in evaluating.
- Detect cycles in hasAsyncGraph walks over linked modules.
- Reject namespace reads while status is linking, matching Node.
- Root module/linker/value across user callbacks and setExport stores;
  restrict setExport to SyntheticModule.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/node_vm/modules.rs`:
- Around line 404-425: Update js_vm_module_link() to enforce the complete linker
contract: only source modules in STATUS_UNLINKED may invoke the linker, while
synthetic STATUS_ERRORED modules must not be reset to STATUS_LINKED. Validate
each linker result as a Module or Promise resolving to a Module, await and
validate Promise results, require the resolved module to share the parent
context and not be errored, and recursively link unlinked modules before storing
them in __vm_linked_modules. Propagate linker throws or validation failures as
rejection and set STATUS_LINKED only after every dependency succeeds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 39894ad5-7689-42db-8aa4-370630fa6047

📥 Commits

Reviewing files that changed from the base of the PR and between 91a1c5c and 11a3fa1.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/node_vm/modules.rs

Comment on lines +404 to +425
if module_kind(hmut::<ObjectHeader>(&module)) == KIND_SYNTHETIC {
set_status(hmut::<ObjectHeader>(&module), STATUS_LINKED);
return undefined_value();
}
if module_status(hmut::<ObjectHeader>(&module)) != STATUS_UNLINKED {
return undefined_value();
}

set_status(hmut::<ObjectHeader>(&module), STATUS_LINKING);
let requests = read_requests(hmut::<ObjectHeader>(&module));
let mut linked = crate::array::js_array_alloc(requests.len() as u32);
for specifier in &requests {
let args = [
string_value(specifier),
module_value.get_nanbox_f64(),
module_request_extra(),
];
let dep = unsafe {
crate::closure::js_native_call_value(linker.get_nanbox_f64(), args.as_ptr(), args.len())
};
linked = crate::array::js_array_push_f64(linked, dep);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline crates/perry-runtime/src/node_vm/modules.rs \
  --items all --type function \
  --match 'js_vm_module_link|js_vm_module_evaluate|js_vm_source_text_module_link_requests'

sed -n '396,455p;481,520p' crates/perry-runtime/src/node_vm/modules.rs

rg -n -C6 'js_vm_module_link|linkRequests|instantiate|module_request_extra|STATUS_LINKING' \
  crates/perry-runtime/src/node_vm.rs \
  crates/perry-runtime/src/node_vm/modules.rs \
  crates/perry-codegen

Repository: PerryTS/perry

Length of output: 21165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '100,160p' crates/perry-runtime/src/node_vm.rs
sed -n '340,460p' crates/perry-runtime/src/node_vm/modules.rs
sed -n '840,900p' crates/perry-runtime/src/node_vm.rs

rg -n "FIELD_CONTEXT|context|resolve|Promise|js_promise|STATUS_ERRORED|evaluate_source_module|evaluate_synthetic_module|module_linked_modules" crates/perry-runtime/src/node_vm.rs crates/perry-runtime/src/node_vm/modules.rs

python3 - <<'PY'
from pathlib import Path
p = Path('crates/perry-runtime/src/node_vm.rs')
text = p.read_text()
for needle in ['js_vm_module_link(', 'module_status', 'STATUS_LINKED', 'evaluate_source_module', 'js_vm_source_text_module_instantiate']:
    print(f'{needle}:', text.count(needle))
PY

Repository: PerryTS/perry

Length of output: 21051


🌐 Web query:

Node.js vm SourceTextModule link linker return module or Promise linkRequests Module.link contract

💡 Result:

In the Node.js vm module, the SourceTextModule.link(linker) method is used to resolve and link the imported dependencies of a module [1][2]. The linker function is a user-provided callback that acts as a mechanism to resolve module specifiers (e.g., import statements) [2][3]. When link is called, it triggers this linker function for every dependency requested by the module [2][4]. Key aspects of the linker contract and process: 1. Linker Signature: The linker function accepts two primary arguments: the specifier (string) of the requested module and the referencingModule (the vm.Module object on which link was called) [2][3]. It may also receive an object containing extra metadata, such as import attributes or assertions [3][5]. 2. Return Value: The linker must return a vm.Module object (or a Promise that resolves to one) [2][5]. 3. Invariants: The returned module must satisfy two strict conditions [3][5]: - It must belong to the same context as the parent module [2][5]. - Its status must not be 'errored' [3][5]. 4. Recursion: If the returned module has an 'unlinked' status, the link method will recursively call the same linker function on that returned module to resolve its dependencies [3][5]. 5. Promise Behavior: The link method itself returns a Promise [3][6]. This Promise resolves once all dependencies have been successfully linked or rejects if the linker function throws an error or returns an invalid module [1][3]. This process effectively implements a host-defined operation similar to the ECMAScript specification's HostResolveImportedModule, with the notable difference that the Node.js linker supports asynchronous resolution [3][5]. Once linking is complete, the module must be instantiated using sourceTextModule.instantiate before it can be evaluated [1][7].

Citations:


Complete the Module.link() contract before marking modules linked.

js_vm_module_link() currently accepts any linker callable result, pushes it into __vm_linked_modules, and sets STATUS_LINKED without awaiting Promise results. This skips Node’s required linker validation:

  • return value must be a Module or a Promise resolving to one
  • resolved modules must share the parent context and not have status errored
  • an unlinked returned module must be recursively linked
  • throwing or invalid links must reject instead of transitioning to STATUS_LINKED

Also don’t reset STATUS_ERRORED synthetic modules to STATUS_LINKED on re-link; and only source modules with STATUS_UNLINKED should perform the linker call path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/node_vm/modules.rs` around lines 404 - 425, Update
js_vm_module_link() to enforce the complete linker contract: only source modules
in STATUS_UNLINKED may invoke the linker, while synthetic STATUS_ERRORED modules
must not be reset to STATUS_LINKED. Validate each linker result as a Module or
Promise resolving to a Module, await and validate Promise results, require the
resolved module to share the parent context and not be errored, and recursively
link unlinked modules before storing them in __vm_linked_modules. Propagate
linker throws or validation failures as rejection and set STATUS_LINKED only
after every dependency succeeds.

Source: Coding guidelines

No code change — kick a fresh workflow batch after ~40m of
ubuntu-latest jobs stuck in queued with no runner assignment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[parity] node:vm — 47 failing node-suite tests (2026-07-22 baseline)

2 participants